package workflow

import (
	"fmt"
	"reflect"
	"strings"
	"sync"
	"time"

	"zstackgo/lib"
	hworkflow "zstackgo/pkg/header/core/workflow"
	"zstackgo/pkg/header/errorcode"

	"github.com/meilihao/golib/v2/log"
)

var (
	_ hworkflow.FlowTrigger      = NewSimpleFlowChain()
	_ hworkflow.FlowRollback     = NewSimpleFlowChain()
	_ hworkflow.FlowChain        = NewSimpleFlowChain()
	_ hworkflow.FlowChainMutable = NewSimpleFlowChain()
)

type flowStopWatch struct {
	beginTime map[string]time.Time
	ref       *SimpleFlowChain
}

func (w *flowStopWatch) Start(flow hworkflow.Flow) {
	btime := time.Now()
	if w.ref.currentFlow != nil {
		name := w.ref.getFlowNameWithoutLocation(w.ref.currentFlow)
		stime := w.beginTime[name]

		w.ref.statisticLock.Lock()
		stat := w.ref.statistics[name]
		w.ref.statisticLock.Unlock()

		stat.AddStatistic(int64(btime.Sub(stime).Seconds()))

		log.Glog.Debug(fmt.Sprintf("[FlowChain(%s):%s, flow:%s] takes %dms to complete", w.ref.id, w.ref.name, name, stat.GetTotalTime()))
	}

	name := w.ref.getFlowNameWithoutLocation(flow)

	w.beginTime[name] = btime

	w.ref.statisticLock.Lock()
	stat := w.ref.statistics[name]
	w.ref.statisticLock.Unlock()

	if stat == nil {
		stat = NewWorkFlowStatistic()
		stat.SetName(name)

		w.ref.statisticLock.Lock()
		w.ref.statistics[name] = stat
		w.ref.statisticLock.Unlock()
	}
}

func (w *flowStopWatch) Stop() {
	etime := time.Now()
	if w.ref.currentFlow != nil {
		name := w.ref.getFlowNameWithoutLocation(w.ref.currentFlow)
		stime := w.beginTime[name]

		w.ref.statisticLock.Lock()
		stat := w.ref.statistics[name]
		w.ref.statisticLock.Unlock()

		stat.AddStatistic(int64(etime.Sub(stime).Seconds()))
	}
}

type SimpleFlowChain struct {
	id                  string
	flows               []hworkflow.Flow
	rollbackFlows       lib.Stack[hworkflow.Flow]
	data                hworkflow.Map
	currentLoop         int
	isStart             bool
	isRollbackStart     bool
	currentFlow         hworkflow.Flow
	currentRollbackFlow hworkflow.Flow
	errorCode           *errorcode.ErrorCode
	contextHandler      hworkflow.FlowContextHandler
	errorHandler        hworkflow.FlowErrorHandler
	doneHandler         hworkflow.FlowDoneHandler
	finallyHandler      hworkflow.FlowFinallyHandler
	name                string
	skipRestRollbacks   bool
	allowEmptyFlow      bool
	flowMarshaller      hworkflow.FlowMarshaller
	processors          []hworkflow.FlowChainProcessor
	preCheck            func(hworkflow.Map) *errorcode.ErrorCode
	afterDone           []*[]func()
	afterError          []*[]func()
	afterFinal          []*[]func()
	isFailCalled        bool
	statistics          map[string]*WorkFlowStatistic
	statisticLock       sync.Mutex
	stopWath            *flowStopWatch
	allowWath           bool
	_lock               sync.Mutex
}

func NewSimpleFlowChain() *SimpleFlowChain {
	return &SimpleFlowChain{
		id:            "FCID_" + "",
		data:          make(hworkflow.Map),
		rollbackFlows: lib.Stack[hworkflow.Flow]{},
	}
}

func NewSimpleFlowChainWithData(data hworkflow.Map) *SimpleFlowChain {
	return &SimpleFlowChain{
		id:            "FCID_" + "",
		data:          data,
		rollbackFlows: lib.Stack[hworkflow.Flow]{},
	}
}

func (c *SimpleFlowChain) AllowWatch() {
	c.allowWath = true
	if c.stopWath == nil {
		c.stopWath = &flowStopWatch{
			beginTime: make(map[string]time.Time),
			ref:       c,
		}
	}
}

func (c *SimpleFlowChain) GetFlows() []hworkflow.Flow {
	return c.flows
}

func (c *SimpleFlowChain) SetFlows(flows []hworkflow.Flow) {
	c.flows = flows
}

func (c *SimpleFlowChain) GetFlowDoneHandler() hworkflow.FlowDoneHandler {
	return c.doneHandler
}

func (c *SimpleFlowChain) SetFlowDoneHandler(handler hworkflow.FlowDoneHandler) {
	c.Done(handler)
}

func (c *SimpleFlowChain) GetFlowErrorHandler() hworkflow.FlowErrorHandler {
	return c.errorHandler
}

func (c *SimpleFlowChain) SetFlowErrorHandler(handler hworkflow.FlowErrorHandler) {
	c.Error(handler)
}

func (c *SimpleFlowChain) GetFlowFinallyHandler() hworkflow.FlowFinallyHandler {
	return c.finallyHandler
}

func (c *SimpleFlowChain) SetFlowFinallyHandler(handler hworkflow.FlowFinallyHandler) {
	c.Finally(handler)
}

func (c *SimpleFlowChain) GetChainName() string {
	return c.name
}

func (c *SimpleFlowChain) SetChainName(name string) {
	c.name = name
}

func (c *SimpleFlowChain) GetChainData() hworkflow.Map {
	return c.data
}

func (c *SimpleFlowChain) SetChainData(data hworkflow.Map) {
	c.SetData(data)
}

func (c *SimpleFlowChain) Insert(flow hworkflow.Flow) hworkflow.FlowChain {
	// ls := make([]hworkflow.Flow, len(c.Flows)+1)
	// ls[0] = flow
	// copy(ls[1:], c.Flows)
	// c.Flows = ls

	return c.InsertPos(0, flow)
}

func (c *SimpleFlowChain) InsertPos(pos int, flow hworkflow.Flow) hworkflow.FlowChain {
	ls := make([]hworkflow.Flow, len(c.flows)+1)
	copy(ls[:pos], c.flows[:pos])
	ls[pos] = flow
	copy(ls[pos+1:], c.flows[pos:])
	c.flows = ls

	return c
}

func (c *SimpleFlowChain) SetFlowMarshaller(marshaller hworkflow.FlowMarshaller) hworkflow.FlowChain {
	c.flowMarshaller = marshaller
	return c
}

// add flow to []flow
func (c *SimpleFlowChain) Then(flow hworkflow.Flow) hworkflow.FlowChain {
	c.flows = append(c.flows, flow)
	return c
}

func (c *SimpleFlowChain) CtxHandler(handler hworkflow.FlowContextHandler) hworkflow.FlowChain {
	if c.errorHandler != nil {
		panic("there has been an FlowContextHandler installed")
	}

	c.contextHandler = handler
	return c
}

func (c *SimpleFlowChain) Error(handler hworkflow.FlowErrorHandler) hworkflow.FlowChain {
	if c.errorHandler != nil {
		panic("there has been an FlowErrorHandler installed")
	}

	c.errorHandler = handler
	return c
}

func (c *SimpleFlowChain) Finally(handler hworkflow.FlowFinallyHandler) hworkflow.FlowChain {
	c.finallyHandler = handler
	return c
}

func (c *SimpleFlowChain) SetData(data hworkflow.Map) hworkflow.FlowChain {
	for k, v := range data {
		c.data[k] = v
	}

	return c
}

func (c *SimpleFlowChain) PutData(k string, v any) hworkflow.FlowChain {
	c.data[k] = v

	return c
}

func (c *SimpleFlowChain) SetName(name string) hworkflow.FlowChain {
	c.name = name
	return c
}

func (c *SimpleFlowChain) PreCheck(checker func(hworkflow.Map) *errorcode.ErrorCode) hworkflow.FlowChain {
	c.preCheck = checker
	return c
}

func (c *SimpleFlowChain) SetProcessors(processors []hworkflow.FlowChainProcessor) {
	c.processors = processors
}

func (c *SimpleFlowChain) GetData() hworkflow.Map {
	return c.data
}

func (c *SimpleFlowChain) Done(handler hworkflow.FlowDoneHandler) hworkflow.FlowChain {
	if c.doneHandler != nil {
		panic("there has been a FlowDoneHandler installed")
	}

	c.doneHandler = handler
	return c
}

var (
	typRunable = reflect.ValueOf(new([]func())).Type()
)

// todo
func (c *SimpleFlowChain) CollectAfterRunable(flow hworkflow.Flow) {
	v := reflect.ValueOf(flow)
	t := v.Type()
	if t.Kind() == reflect.Ptr {
		v = v.Elem()
		t = v.Type()
	}

	tp, ok := t.FieldByName("AfterDone")
	if ok && tp.Type == typRunable {
		c.afterDone = append(c.afterDone, v.FieldByIndex(tp.Index).Interface().(*[]func()))
	}

	tp, ok = t.FieldByName("AfterError")
	if ok && tp.Type == typRunable {
		c.afterError = append(c.afterError, v.FieldByIndex(tp.Index).Interface().(*[]func()))
	}

	tp, ok = t.FieldByName("AfterFinal")
	if ok && tp.Type == typRunable {
		c.afterFinal = append(c.afterFinal, v.FieldByIndex(tp.Index).Interface().(*[]func()))
	}
}

func (c *SimpleFlowChain) runFlow(flow hworkflow.Flow) {
	var toRun hworkflow.Flow // which flow to run
	if c.flowMarshaller != nil {
		var pre string
		if c.currentFlow != nil {
			pre = c.currentFlow.GetName()
		}

		toRun = c.flowMarshaller.MarshalTheNextFlow(pre, flow.GetName(), c, c.data)
		if toRun != nil {
			log.Glog.Debug(fmt.Sprintf("[FlowChain(%s): %s] flowMarshaller[%s] replaces the next flow[%s] to the flow[%s]",
				c.id, c.name, c.flowMarshaller.GetName(), flow.GetName(), toRun.GetName()))
		}
	}
	if toRun == nil {
		toRun = flow
	}

	if c.allowWath {
		c.stopWath.Start(toRun)
	}

	c.currentFlow = toRun

	flowName := c.getFlowName(c.currentFlow)
	log.Glog.Debug(fmt.Sprintf("[FlowChain(%s): %s] start executing flow[%s]", c.id, c.name, flowName))
	c.CollectAfterRunable(toRun) // allow afterDone/afterError/afterFinal in flow

	if c.preCheck != nil {
		log.Glog.Debug(fmt.Sprintf("[FlowChain(%s): %s] start executing pre-check for flow[%s]", c.id, c.name, flowName))
		if er := c.preCheck(c.data); er != nil {
			c.Fail(er)
			return
		}
	}
	if c.isSkipFlow(toRun) {
		c.Next()
	} else {
		if c.contextHandler != nil {
			c.contextHandler.SaveContext()
			if c.contextHandler.Cancelled() {
				c.Fail(c.contextHandler.GetCancelError())
				return
			}
		} else {
			if err := toRun.Run(c, c.data); err != nil {
				c.Fail(&errorcode.ErrorCode{Details: err.Error()})
			}
		}
	}

}

func (c *SimpleFlowChain) rollbackFlow(flow hworkflow.Flow) {
	log.Glog.Debug(fmt.Sprintf("[FlowChain(%s): %s] start to rollback flow[%s]", c.id, c.name, ""))
	c.currentLoop--

	if c.contextHandler != nil {
		if c.contextHandler.SkipRollback(c.GetErrorCode()) {
			c.SkipRestRollbacks()
			c.Rollback()
			return
		}
	}

	if er := flow.Rollback(c, c.data); er != nil {
		log.Glog.Warn(fmt.Sprintf("[FlowChain(%s): %s] unhandled exception when rollback flow[%s],"+
			" continue to next rollback: %v", c.id, c.name, flow.GetName(), er))
		c.Rollback()
	}
}

func Reverse(ls []*[]func()) []*[]func() {
	tmp := make([]*[]func(), 0, len(ls))
	for n := len(ls) - 1; n >= 0; n-- {
		tmp = append(tmp, ls[n])
	}
	return tmp
}

func (c *SimpleFlowChain) callErrorHandler(info bool) {
	if info {
		log.Glog.Debug(fmt.Sprintf("[FlowChain(%s): %s] rolled back all flows because error%v", c.id, c.name, c.errorCode))
	}

	if c.errorHandler != nil {
		if err := c.errorHandler.Handle(c.errorCode, c.data); err != nil {
			log.Glog.Warn(fmt.Sprintf("unhandled exception when calling %s, %v", c.errorHandler.GetName(), err))
		}
	}

	if len(c.afterError) != 0 {
		Reverse(c.afterError)

		for _, errors := range c.afterError {
			for _, arg := range *errors {
				arg()
			}
		}
	}

	c.callFinallyHandler()
}

func (c *SimpleFlowChain) getFlowNameWithoutLocation(flow hworkflow.Flow) string {
	name := c.getFlowName(flow)
	if i := strings.Index(name, " (location:"); i != -1 {
		return name[:i]
	}
	return name
}

func (c *SimpleFlowChain) getFlowName(flow hworkflow.Flow) string {
	name := flow.GetName()

	name += fmt.Sprintf(" (location:%d/%d)", c.currentLoop, len(c.flows))

	return name
}

func (c *SimpleFlowChain) Rollback() {
	if !c.isFailCalled {
		panic("Rollback() cannot be called before Fail() is called")
	}

	c.isRollbackStart = true
	if c.rollbackFlows.Len() == 0 {
		c.callErrorHandler(true)
		return
	}

	if c.skipRestRollbacks {
		var restRollbackNames []string
		for _, v := range c.rollbackFlows.Vals {
			restRollbackNames = append(restRollbackNames, v.GetName())
		}
		log.Glog.Debug(fmt.Sprintf("[FlowChain(%s): %s] we are instructed to skip rollbacks for remaining flows%v",
			c.id, c.name, restRollbackNames))
		c.callErrorHandler(true)
		return
	}

	if c.currentRollbackFlow != nil {
		log.Glog.Debug(fmt.Sprintf("[FlowChain(%s): %s] successfully rolled back flow[%s]",
			c.id, c.name, c.getFlowName(c.currentRollbackFlow)))
	} else {
		log.Glog.Debug(fmt.Sprintf("[FlowChain(%s): %s] start to rollback", c.id, c.name))
	}

	flow, ok := c.rollbackFlows.Pop()
	if ok {
		c.currentRollbackFlow = flow
		c.rollbackFlow(flow)
	}
}

func (c *SimpleFlowChain) SkipRestRollbacks() {
	c.skipRestRollbacks = true
}

func (c *SimpleFlowChain) GetErrorCode() *errorcode.ErrorCode {
	return c.errorCode
}

func (c *SimpleFlowChain) SetError(er *errorcode.ErrorCode) {
	c.setErrorCode(er)
}

func (c *SimpleFlowChain) callFinallyHandler() {
	if c.finallyHandler != nil {
		if er := c.finallyHandler.Finally(); er != nil {
			log.Glog.Warn(fmt.Sprintf("unhandled exception when calling %s, %v", c.finallyHandler.GetName(), er))
		}
	}

	if len(c.afterFinal) != 0 {
		Reverse(c.afterFinal)

		for _, fianls := range c.afterFinal {
			for _, f := range *fianls {
				f()
			}
		}
	}
}

func (c *SimpleFlowChain) callDoneHandler() {
	if c.allowWath {
		c.stopWath.Stop()
	}

	if c.doneHandler != nil {
		if er := c.doneHandler.Handle(c.data); er != nil {
			log.Glog.Warn(fmt.Sprintf("unhandled exception when calling %s, %v", c.doneHandler.GetName(), er))
		}
	}

	log.Glog.Debug(fmt.Sprintf("[FlowChain(%s): %s] successfully completed", c.id, c.name))

	if len(c.afterDone) != 0 {
		Reverse(c.afterDone)

		for _, fianls := range c.afterDone {
			for _, f := range *fianls {
				f()
			}
		}
	}

	c.callFinallyHandler()
}

func (c *SimpleFlowChain) Fail(er *errorcode.ErrorCode) {
	c.isFailCalled = true
	c.setErrorCode(er)
	c.rollbackFlows.Push(c.currentFlow)
	c.Rollback() // do rollback
}

func (c *SimpleFlowChain) isSkipFlow(flow hworkflow.Flow) (isSkip bool) {
	isSkip = flow.Skip(c.data)
	if isSkip {
		log.Glog.Debug(fmt.Sprintf("[FlowChain: %s] skip flow[%s] because it's skip() returns true", c.name, ""))
	}
	return
}

func (c *SimpleFlowChain) runFlowOrComplete() {
	if c.currentLoop < len(c.flows) {
		c.currentLoop++
		c.runFlow(c.flows[c.currentLoop-1])
	} else {
		if c.GetErrorCode() == nil {
			c.callDoneHandler()
		} else {
			c.callErrorHandler(false)
		}
	}
}

func (c *SimpleFlowChain) Next() {
	if !c.isStart { // must after started
		panic(fmt.Sprintf("[FlowChain(%s): %s] you must call start() first, and only call next() in Flow.run()",
			c.id, c.name))
	}
	if c.isRollbackStart { // must before rollbacked
		panic(fmt.Sprintf("[FlowChain(%s): %s] rollback has started, you can't call next()",
			c.id, c.name))
	}
	// push current flow to stack for rollback
	c.rollbackFlows.Push(c.currentFlow)

	log.Glog.Debug(fmt.Sprintf("[FlowChain(%s): %s] successfully executed flow[%s]", c.id, c.name, c.getFlowName(c.currentFlow)))

	c.runFlowOrComplete()
}

func (c *SimpleFlowChain) Start() {
	if c.processors != nil {
		for _, p := range c.processors {
			p.ProcessFlowChain(c)
		}
	}

	if len(c.flows) == 0 && c.allowEmptyFlow {
		c.callDoneHandler()
		return
	}

	if len(c.flows) == 0 {
		panic("you must call then() to add flow before calling start() or allowEmptyFlow() to run empty flow chain on purpose")
	}

	if c.data == nil {
		c.data = hworkflow.Map{}
	}

	c.isStart = true
	if c.name == "" {
		c.name = "anonymous-chain"
	}

	log.Glog.Debug(fmt.Sprintf("[FlowChain(%s): %s] starts", c.id, c.name))

	var names []string
	for _, v := range c.flows {
		names = append(names, fmt.Sprintf("%s[%s]", v.GetName(), c.getFlowNameWithoutLocation(v)))
	}
	log.Glog.Debug(fmt.Sprintf("execution path:\n%s", strings.Join(names, " -->\n")))

	c.runFlowOrComplete()
}

func (c *SimpleFlowChain) NoRollback(no bool) hworkflow.FlowChain {
	c.skipRestRollbacks = no
	return c
}

func (c *SimpleFlowChain) AllowEmptyFlow() hworkflow.FlowChain {
	c.allowEmptyFlow = true
	return c
}

func (c *SimpleFlowChain) setErrorCode(er *errorcode.ErrorCode) {
	c.errorCode = er
	if er != nil {
		c.errorCode.SetLocation(c.getFlowName(c.currentFlow))
	}
}

func (c *SimpleFlowChain) GetStatistics() map[string]*WorkFlowStatistic {
	return c.statistics
}
